Skip to content

structured logging: JSON and journald#2897

Draft
darkexplosiveqwx wants to merge 11 commits into
pi-hole:developmentfrom
darkexplosiveqwx:log-json
Draft

structured logging: JSON and journald#2897
darkexplosiveqwx wants to merge 11 commits into
pi-hole:developmentfrom
darkexplosiveqwx:log-json

Conversation

@darkexplosiveqwx
Copy link
Copy Markdown
Contributor

@darkexplosiveqwx darkexplosiveqwx commented May 21, 2026

See pi-hole/docker-pi-hole#2036 for more information.

Major issue:
dnsmasq closes the file descriptors of STDIN, STDOUT and STDERR, this prevents us from logging after dnsmasq has started. Removing the lines is only a quick duct-tape fix while developing.

Removing that code seems to be the best option, as it also doesn’t affect other functionality.

This can also be observed in the systemd service log, since it only contains startup information and not the entire FTL.log.
In docker we are not capturing stdout, but tailing the logfile, but I intentionally wanted to keep the logfiles as-is, need to find a solution on how to implement this.

This currently changes webserver.log and the dnsmasq and webserver FIFOs to better align with FTL.log, but this can be reverted if necessary.

What does this PR aim to accomplish?:

How does this PR accomplish the above?:

Link documentation PRs if any are needed to support this PR:


By submitting this pull request, I confirm the following:

  1. I have read and understood the contributors guide, as well as this entire template. I understand which branch to base my commits and Pull Requests against.
  2. I have commented my proposed changes within the code and I have tested my changes.
  3. I am willing to help maintain this change if there are issues with it later.
  4. It is compatible with the EUPL 1.2 license
  5. I have squashed any insignificant commits. (git rebase)
  6. I have checked that another pull request for this purpose does not exist.
  7. I have considered, and confirmed that this submission will be valuable to others.
  8. I accept that this submission may not be used, and the pull request closed at the will of the maintainer.
  9. I give this submission freely, and claim no ownership to its content.

  • I have read the above and my PR is ready for review. Check this box to confirm

@darkexplosiveqwx
Copy link
Copy Markdown
Contributor Author

Both --log-json and --log-journal are functional to some degree and can be tested further.

I think that we can now start discussing what extra fields we might want/need. (Like the file paths shown in debug flags)

Regarding the blunt patch into dnsmasq to prevent it from messing with our file descriptors, I definitely need to guidance here on whether this is a good mean or if there is a better way.

@darkexplosiveqwx darkexplosiveqwx changed the title structured JSON logging structured logging: JSON and journald May 23, 2026
@pralor-bot
Copy link
Copy Markdown

This pull request has been mentioned on Pi-hole Userspace. There might be relevant details there:

https://discourse.pi-hole.net/t/use-systemd-journalctl-for-all-logs/84443/21

@darkexplosiveqwx darkexplosiveqwx force-pushed the log-json branch 2 times, most recently from 8e7fd91 to 7846900 Compare May 27, 2026 22:24
We never read from FTL.log and webserver.log after opening them, so
+read is unnecessary.

Signed-off-by: darkexplosiveqwx <101737077+darkexplosiveqwx@users.noreply.github.com>
Signed-off-by: darkexplosiveqwx <101737077+darkexplosiveqwx@users.noreply.github.com>
carries to: FIFO, webserver.log
changes webserver.log format (again)

Signed-off-by: darkexplosiveqwx <101737077+darkexplosiveqwx@users.noreply.github.com>
Signed-off-by: darkexplosiveqwx <101737077+darkexplosiveqwx@users.noreply.github.com>
Signed-off-by: darkexplosiveqwx <101737077+darkexplosiveqwx@users.noreply.github.com>
Signed-off-by: darkexplosiveqwx <101737077+darkexplosiveqwx@users.noreply.github.com>
they are not useful here as they would only show the component again

Signed-off-by: darkexplosiveqwx <101737077+darkexplosiveqwx@users.noreply.github.com>
This allows shipping fully static binaries with --log-journal support

Signed-off-by: darkexplosiveqwx <101737077+darkexplosiveqwx@users.noreply.github.com>
Signed-off-by: darkexplosiveqwx <101737077+darkexplosiveqwx@users.noreply.github.com>
Signed-off-by: darkexplosiveqwx <101737077+darkexplosiveqwx@users.noreply.github.com>
This helps the clangd LSP find headers and improves expericence in
editors/IDEs

Signed-off-by: darkexplosiveqwx <101737077+darkexplosiveqwx@users.noreply.github.com>
@yubiuser
Copy link
Copy Markdown
Member

yubiuser commented Jun 3, 2026

We probably don't want all dnsmasq log to be printed when using --log-json

(This was a container without any queries received...)

pihole  | {"timestamp":"2026-06-03T14:19:03.330Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"query[A] pi.hole from 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:19:03.330Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"Pi-hole hostname pi.hole is 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:19:33.381Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"query[A] pi.hole from 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:19:33.381Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"Pi-hole hostname pi.hole is 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:20:03.442Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"query[A] pi.hole from 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:20:03.442Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"Pi-hole hostname pi.hole is 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:20:33.498Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"query[A] pi.hole from 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:20:33.499Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"Pi-hole hostname pi.hole is 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:21:03.589Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"query[A] pi.hole from 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:21:03.590Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"Pi-hole hostname pi.hole is 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:21:33.650Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"query[A] pi.hole from 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:21:33.650Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"Pi-hole hostname pi.hole is 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:22:03.702Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"query[A] pi.hole from 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:22:03.702Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"Pi-hole hostname pi.hole is 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:22:33.757Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"query[A] pi.hole from 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:22:33.758Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"Pi-hole hostname pi.hole is 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:23:03.812Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"query[A] pi.hole from 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:23:03.812Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"Pi-hole hostname pi.hole is 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:23:33.868Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"query[A] pi.hole from 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:23:33.868Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"Pi-hole hostname pi.hole is 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:24:03.923Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"query[A] pi.hole from 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:24:03.923Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"Pi-hole hostname pi.hole is 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:24:33.985Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"query[A] pi.hole from 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:24:33.986Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"Pi-hole hostname pi.hole is 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:25:04.034Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"query[A] pi.hole from 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:25:04.034Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"Pi-hole hostname pi.hole is 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:25:34.084Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"query[A] pi.hole from 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:25:34.084Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"Pi-hole hostname pi.hole is 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:26:04.133Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"query[A] pi.hole from 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:26:04.133Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"Pi-hole hostname pi.hole is 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:26:34.183Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"query[A] pi.hole from 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:26:34.183Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"Pi-hole hostname pi.hole is 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:27:04.237Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"query[A] pi.hole from 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:27:04.237Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"Pi-hole hostname pi.hole is 127.0.0.1"}
pihole  | {"timestamp":"2026-06-03T14:27:34.328Z","log_level":"INFO","service":"pihole-FTL","component":"dnsmasq","pid":"127M","message":"query[A] pi.hole from 127.0.0.1"}

@rdwebdesign
Copy link
Copy Markdown
Member

These are normal lines from pihole.log.

2026-06-03 16:21:16.922 query[A] pi.hole from 127.0.0.1
2026-06-03 16:21:16.923 Pi-hole hostname pi.hole is 192.168.0.201

If I'm not mistaken, they are generated by the container health check, every 30 seconds.

Do you think pihole.log should always be in plain text?

@yubiuser
Copy link
Copy Markdown
Member

yubiuser commented Jun 3, 2026

Do you think pihole.log should always be in plain text?

No. I think we should have an option to disable dnsmasq logs - imagine the example above with a few hundred queries from a real instance.

@darkexplosiveqwx
Copy link
Copy Markdown
Contributor Author

darkexplosiveqwx commented Jun 3, 2026

Do you think pihole.log should always be in plain text?

I'm not sure if I understand the question, but what else should the logfile be besides plain text?

I do think that we should have all logs available with --log-json/--log-journal, this is why we can filter with priority and component.

Maybe we can add an option to exclude query-level information, but this information would also be added to pihole.log anyway.

Disk usage wise, I don't think it's so bad. (I have been using --log-journal for the last couple days with multiple active mobile devices.)

$ journalctl -b -u pihole-FTL.service | wc -l  # around 26h
449014

$ journalctl -u pihole-FTL.service | wc -l  # since May 24 11:57:49
2399128

$ journalctl --disk-usage
Archived and active journals take up 2.6G in the file system.

I think the ability to just filter for FTL logs again offsets the raw verbosity.

Edit: how it compares to other services:

$ journalctl -o json | jq -r '._SYSTEMD_UNIT // "unknown"'  | sort | uniq -c | sort -nr

2406426 pihole-FTL.service
2125350 unbound.service
 975965 wpa_supplicant@wlu1bros.service
 299943 tailscaled.service
 271666 user@1000.service
  99082 init.scope
  97450 unknown
  37606 hostapd@wlo0c.service
  31435 systemd-networkd.service
  21173 cron.service
  11202 iwd.service
   8259 avahi-daemon.service
   7550 caddy.service
   7129 update-dhcp6pd-set@eno0c.service
   4457 systemd-timesyncd.service
   3567 pihole-update-gravity.service
   3534 hostapd@wlu1ap.service

@rdwebdesign
Copy link
Copy Markdown
Member

rdwebdesign commented Jun 3, 2026

@yubiuser

imagine the example above with a few hundred queries from a real instance.

Currently (in master), these lines are already NOT printed to docker logs. There is no need to add them.

I think there are 2 distinct ideas being developed together:

  1. The idea of adding an option to format Pi-hole logs as JSON.
    Personally, I think if an user decides to use JSON format, all logs (FTL.log, webserver.log and pihole.log) should use this format.

  2. The idea of generating docker logs using JSON format.
    If I understood the idea correctly, this would send to docker log, the same lines we already send today, but formatted as JSON.
    This PR is changing what docker log "sees", also sending pihole.log lines to docker log.
    I don't think we need this.

@darkexplosiveqwx
Copy link
Copy Markdown
Contributor Author

darkexplosiveqwx commented Jun 3, 2026

@yubiuser

imagine the example above with a few hundred queries from a real instance.

Currently (in master), these lines are already NOT printed to docker logs. There is no need to add them.

I think there are 2 distinct ideas being developed together:

  1. The idea of adding an option to format Pi-hole logs as JSON.

Personally, I think if an user decides to use JSON format, all logs (FTL.log, webserver.log and pihole.log) should use this format.

  1. The idea of generating docker logs using JSON format.

If I understood the idea correctly, this would send to docker log, the same lines we already send today, but formatted as JSON.

This PR is changing what docker log "sees", also sending pihole.log lines to docker log.

I don't think we need this.

My idea was to not change the logfiles to JSON, but instead have a unified JSON stream in stdout where all three are included, which allows filtering with component.

By sending all logs to docker stdout, users (or rather machines) won't have to touch the files at all anymore and can just parse and filter the stdout.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants